/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.schema;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import javax.xml.xpath.XPathConstants;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.util.BytesRef;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.util.LogListener;
import org.apache.solr.util.TestHarness;
import org.junit.BeforeClass;
import org.junit.Ignore;

public class RankFieldTest extends SolrTestCaseJ4 {

  private static final String RANK_1 = "rank_1";
  private static final String RANK_2 = "rank_2";

  @BeforeClass
  public static void beforeClass() throws Exception {
    initCore("solrconfig-minimal.xml", "schema-rank-fields.xml");
  }

  @Override
  public void setUp() throws Exception {
    clearIndex();
    assertU(commit());
    super.setUp();
  }

  public void testInternalFieldName() {
    assertEquals(
        "RankField.INTERNAL_RANK_FIELD_NAME changed in an incompatible way",
        "_rank_",
        RankField.INTERNAL_RANK_FIELD_NAME);
  }

  public void testBasic() {
    assertNotNull(h.getCore().getLatestSchema().getFieldOrNull(RANK_1));
    assertEquals(
        RankField.class, h.getCore().getLatestSchema().getField(RANK_1).getType().getClass());
  }

  public void testBadFormat() {
    try (LogListener errors =
        LogListener.error(RequestHandlerBase.class).substring("Expecting float")) {
      assertFailedU(adoc("id", "1", RANK_1, "foo"));

      assertFailedU(adoc("id", "1", RANK_1, "1.2.3"));
      assertEquals(2, errors.getCount());
      errors.clearQueue();
    }

    try (LogListener errors =
        LogListener.error(RequestHandlerBase.class).substring("must be finite")) {
      assertFailedU(adoc("id", "1", RANK_1, Float.toString(Float.POSITIVE_INFINITY)));

      assertFailedU(adoc("id", "1", RANK_1, Float.toString(Float.NEGATIVE_INFINITY)));

      assertFailedU(adoc("id", "1", RANK_1, Float.toString(Float.NaN)));
      assertEquals(3, errors.getCount());
      errors.clearQueue();
    }

    try (LogListener errors =
        LogListener.error(RequestHandlerBase.class).substring("must be a positive")) {
      assertFailedU(adoc("id", "1", RANK_1, Float.toString(-0.0f)));

      assertFailedU(adoc("id", "1", RANK_1, Float.toString(-1f)));

      assertFailedU(adoc("id", "1", RANK_1, Float.toString(0.0f)));
      assertEquals(3, errors.getCount());
      errors.clearQueue();
    }
  }

  public void testAddRandom() {
    for (int i = 0; i < random().nextInt(TEST_NIGHTLY ? 10000 : 100); i++) {
      assertU(adoc("id", String.valueOf(i), RANK_1, Float.toString(random().nextFloat())));
    }
    assertU(commit());
  }

  public void testSkipEmpty() {
    assertU(adoc("id", "1", RANK_1, ""));
  }

  public void testBasicAdd() throws IOException {
    assertU(adoc("id", "testBasicAdd", RANK_1, "1"));
    assertU(commit());
    // assert that the document made it in
    assertQ(req("q", "id:testBasicAdd"), "//*[@numFound='1']");
    h.getCore()
        .withSearcher(
            (searcher) -> {
              LeafReader reader = searcher.getIndexReader().getContext().leaves().get(0).reader();
              // assert that the field made it in
              assertNotNull(reader.getFieldInfos().fieldInfo(RankField.INTERNAL_RANK_FIELD_NAME));
              // assert that the feature made it in
              assertTrue(
                  reader
                      .terms(RankField.INTERNAL_RANK_FIELD_NAME)
                      .iterator()
                      .seekExact(new BytesRef(RANK_1.getBytes(StandardCharsets.UTF_8))));
              return null;
            });
  }

  public void testMultipleRankFields() throws IOException {
    assertU(adoc("id", "testMultiValueAdd", RANK_1, "1", RANK_2, "2"));
    assertU(commit());
    // assert that the document made it in
    assertQ(req("q", "id:testMultiValueAdd"), "//*[@numFound='1']");
    h.getCore()
        .withSearcher(
            (searcher) -> {
              LeafReader reader = searcher.getIndexReader().getContext().leaves().get(0).reader();
              // assert that the field made it in
              assertNotNull(reader.getFieldInfos().fieldInfo(RankField.INTERNAL_RANK_FIELD_NAME));
              // assert that the features made it in
              assertTrue(
                  reader
                      .terms(RankField.INTERNAL_RANK_FIELD_NAME)
                      .iterator()
                      .seekExact(new BytesRef(RANK_2.getBytes(StandardCharsets.UTF_8))));
              assertTrue(
                  reader
                      .terms(RankField.INTERNAL_RANK_FIELD_NAME)
                      .iterator()
                      .seekExact(new BytesRef(RANK_1.getBytes(StandardCharsets.UTF_8))));
              return null;
            });
  }

  public void testSortFails() {
    assertU(adoc("id", "testSortFails", RANK_1, "1"));
    assertU(commit());
    assertQEx(
        "Can't sort on rank field", req("q", "id:testSortFails", "sort", RANK_1 + " desc"), 400);
  }

  @Ignore("We currently don't fail these kinds of requests with other field types")
  public void testFacetFails() {
    assertU(adoc("id", "testFacetFails", RANK_1, "1"));
    assertU(commit());
    assertQEx(
        "Can't facet on rank field",
        req(
            "q", "id:testFacetFails",
            "facet", "true",
            "facet.field", RANK_1),
        400);
  }

  public void testTermQuery() {
    assertU(adoc("id", "testTermQuery", RANK_1, "1", RANK_2, "1"));
    assertU(adoc("id", "testTermQuery2", RANK_1, "1"));
    assertU(commit());
    assertQ(req("q", RANK_1 + ":*"), "//*[@numFound='2']");
    assertQ(req("q", RANK_1 + ":[* TO *]"), "//*[@numFound='2']");
    assertQ(req("q", RANK_2 + ":*"), "//*[@numFound='1']");
    assertQ(req("q", RANK_2 + ":[* TO *]"), "//*[@numFound='1']");

    assertQEx("Term queries not supported", req("q", RANK_1 + ":1"), 400);
    assertQEx("Range queries not supported", req("q", RANK_1 + ":[1 TO 10]"), 400);
  }

  public void testResponseQuery() {
    assertU(adoc("id", "testResponseQuery", RANK_1, "1"));
    assertU(commit());
    // Ignore requests to retrieve rank
    assertQ(
        req("q", RANK_1 + ":*", "fl", "id," + RANK_1),
        "//*[@numFound='1']",
        "count(//result/doc[1]/str)=1");
  }

  public void testRankQParserQuery() {
    assertU(adoc("id", "1", "str_field", "foo", RANK_1, "1", RANK_2, "2"));
    assertU(adoc("id", "2", "str_field", "foo", RANK_1, "2", RANK_2, "1"));
    assertU(commit());
    assertQ(
        req(
            "q",
            "str_field:foo _query_:{!rank f='" + RANK_1 + "' function='log' scalingFactor='1'}"),
        "//*[@numFound='2']",
        "//result/doc[1]/str[@name='id'][.='2']",
        "//result/doc[2]/str[@name='id'][.='1']");

    assertQ(
        req(
            "q",
            "str_field:foo _query_:{!rank f='" + RANK_2 + "' function='log' scalingFactor='1'}"),
        "//*[@numFound='2']",
        "//result/doc[1]/str[@name='id'][.='1']",
        "//result/doc[2]/str[@name='id'][.='2']");

    assertQ(
        req(
            "q",
            "foo",
            "defType",
            "dismax",
            "qf",
            "str_field^10",
            "bq",
            "{!rank f='" + RANK_1 + "' function='log' scalingFactor='1'}"),
        "//*[@numFound='2']",
        "//result/doc[1]/str[@name='id'][.='2']",
        "//result/doc[2]/str[@name='id'][.='1']");

    assertQ(
        req(
            "q",
            "foo",
            "defType",
            "dismax",
            "qf",
            "str_field^10",
            "bq",
            "{!rank f='" + RANK_2 + "' function='log' scalingFactor='1'}"),
        "//*[@numFound='2']",
        "//result/doc[1]/str[@name='id'][.='1']",
        "//result/doc[2]/str[@name='id'][.='2']");
  }

  public void testScoreChanges() throws Exception {
    assertU(adoc("id", "1", "str_field", "foo", RANK_1, "1"));
    assertU(commit());
    ModifiableSolrParams params =
        params(
            "q", "foo", "defType", "dismax", "qf", "str_field^10", "fl", "id,score", "wt", "xml");

    double scoreBefore =
        (Double)
            TestHarness.evaluateXPath(
                h.query(req(params)),
                "//result/doc[1]/float[@name='score']",
                XPathConstants.NUMBER);
    params.add("bq", "{!rank f='" + RANK_1 + "' function='log' scalingFactor='1'}");
    double scoreAfter =
        (Double)
            TestHarness.evaluateXPath(
                h.query(req(params)),
                "//result/doc[1]/float[@name='score']",
                XPathConstants.NUMBER);
    assertNotEquals("Expecting score to change", scoreBefore, scoreAfter, 0f);
  }
}
